#include <iostream>
#include <string>

#include <boost/algorithm/string/erase.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <gflags/gflags.h>
#include <nlohmann/json.hpp>

#include "coin2/app/init.h"
#include "coin2/base/date_util.h"
#include "coin2/exchange/base/order/symbol_cache.h"
#include "coin2/exchange/base/symbology/derivative_util.h"
#include "coin2/exchange/base/symbology/product_encyclopedia.h"
#include "coin2/exchange/base/symbology/product_holder_manager.h"
#include "coin2/exchange/factory/symbology/product.h"
#include "naviasync/appinfo/build_info.h"

DEFINE_string(mea, "", "Specify market.exchange.api_version.");
DEFINE_string(timestamp, "", "Specify trading date in ts(nano) or date string.");
DEFINE_string(native_symbol, "", "native symbol");
DEFINE_string(norm_symbol, "", "norm symbol");
DEFINE_string(json_file, "", "Specify json file to load.");
DEFINE_bool(check_holder, true, "check holder or not.");
DEFINE_string(contract_type, "", "contract type");
DEFINE_double(qty, 0.00, "Get the formatted qty of the product");
DEFINE_double(price, 0.00, "Get the formatted price of the product");
DEFINE_bool(show_version, false, "show git hash/version");

namespace {

using coin::proto::ProductHolderManagerConfig;
using coin2::exchange::base::order::SymbologyCache;
using coin2::exchange::base::symbology::ICurrency;
using coin2::exchange::base::symbology::IProduct;
using coin2::exchange::base::symbology::IProductHolder;
using coin2::exchange::base::symbology::ProductHolderManager;
using GetProductHolderFunc = std::function<const IProductHolder*(const IProduct&, int64_t)>;
using ProductEncyclopedia = coin2::exchange::base::symbology::ProductEncyclopedia;
static constexpr int64_t day = 3600 * 24 * 1'000'000'000LL;
inline int64_t TimestampToUtc00(int64_t ts) { return (ts / day) * day; }
}  // namespace

std::shared_ptr<SymbologyCache> CreateSymbCache() {
  ProductHolderManagerConfig config;
  auto product_holder_manager = std::make_shared<ProductHolderManager>(config);
  GetProductHolderFunc get_product_holder =
      [product_holder_manager](const IProduct& product, int64_t ts) {
        return product_holder_manager->GetProductHolder(product, ts);
      };

  auto symcache = std::make_shared<SymbologyCache>();
  symcache->SetGetProductHolder(get_product_holder);
  // std::unique_ptr<IProduct> product_clone(product->Clone());
  // symcache.AddProduct(std::move(product_clone));
  return symcache;
}

void CheckProductParser(const IProduct& product, const nlohmann::json& elem) {
  // CHECK_EQ(product.relative_norm(), elem["symbol"]);
  // CHECK_EQ(product.native_symbol(), elem["native_symbol"]);
  const ICurrency& base = product.base();
  const ICurrency& quote = product.quote();
  CHECK_EQ(base.symbol(), elem["base"]);
  CHECK_EQ(base.native_symbol(), elem["native_base"]);
  CHECK_EQ(quote.symbol(), elem["quote"]);
  CHECK_EQ(quote.native_symbol(), elem["native_quote"]);
}

void TestProductFromNativeString(
    const MarketExchangeApi& mea,
    int64_t timestamp,
    const std::string& native_symbol,
    ProductEncyclopedia& product_cache,
    const nlohmann::json* elem = nullptr) {
  nlohmann::json input;
  nlohmann::json output;
  nlohmann::json json;

  input["timestamp"] = timestamp;
  input["mea"] = mea.String();
  input["native_symbol"] = native_symbol;

  auto product = CreateProductFromNativeString(mea, native_symbol, timestamp, false);
  if (product) {
    CHECK_EQ(product->resolve_timestamp(), timestamp);
    if (elem != nullptr) {
      CheckProductParser(*product.get(), *elem);
    }

    if (product->exchange() == coin::proto::Bithumb) {
      bool a = (StringToUpperCopy(product->native_symbol()) == StringToUpperCopy(native_symbol));
      bool b =
          (StringToUpperCopy(product->native_symbol()) ==
           StringToUpperCopy(native_symbol) + "_KRW");
      CHECK(a || b) << StringToUpperCopy(product->native_symbol()) << ", "
                    << StringToUpperCopy(native_symbol);
    } else if (product->exchange() == coin::proto::Coinone) {
      bool a = (StringToUpperCopy(product->native_symbol()) == StringToUpperCopy(native_symbol));
      bool b =
          (StringToUpperCopy(product->native_symbol()) ==
           StringToUpperCopy(native_symbol) + "-KRW");
      bool c =
          (StringToUpperCopy(product->native_symbol()) + "-KRW" ==
           StringToUpperCopy(native_symbol));
      CHECK(a || b || c) << StringToUpperCopy(product->native_symbol()) << ", "
                         << StringToUpperCopy(native_symbol);
    } else if (product->exchange() == coin::proto::Btcturk) {
      std::string temp = native_symbol;
      boost::erase_all(temp, "_");
      CHECK_EQ(StringToUpperCopy(product->native_symbol()), StringToUpperCopy(temp))
          << StringToUpperCopy(product->native_symbol()) << ", "
          << StringToUpperCopy(native_symbol);
    } else {
      CHECK_EQ(StringToUpperCopy(product->native_symbol()), StringToUpperCopy(native_symbol))
          << StringToUpperCopy(product->native_symbol()) << ", "
          << StringToUpperCopy(native_symbol);
    }

    if (FLAGS_check_holder) {
      auto& holder = product_cache.holder(*product);
      // CHECK_LE(std::abs(holder.product().resolve_timestamp() - timestamp), 30'000'000'000LL)
      //   << holder.product().resolve_timestamp() << ", " << timestamp;
      CHECK_EQ(TimestampToUtc00(holder.product().resolve_timestamp()), TimestampToUtc00(timestamp))
          << holder.product().resolve_timestamp() << ", " << timestamp;

      CHECK_EQ(holder.product().absolute_norm(), product->absolute_norm())
          << holder.product().absolute_norm() << ", " << product->absolute_norm();

      if (mea.exchange == coin::proto::Deribit && mea.market == coin::proto::Options) {
        // pass
      } else {
        CHECK_EQ(holder.product().relative_norm(), product->relative_norm())
            << holder.product().relative_norm() << ", " << product->relative_norm();
      }
    }

    output["native_symbol"] = product->native_symbol();
    output["absolute_norm"] = product->absolute_norm();
    output["relative_norm"] = product->relative_norm();
    output["base"] = product->base().currency();
    output["quote"] = product->quote().currency();
    output["native_base"] = product->base().native_currency();
    output["native_quote"] = product->quote().native_currency();
  } else {
    CHECK(false) << coin::proto::MarketType_Name(mea.market) + ":" + coin::proto::ExchangeType_Name(mea.exchange) + ":" + native_symbol;
  }

  json["input"] = input;
  json["output"] = output;
  std::cout << json.dump() << std::endl;
}

void TestProductFromNormString(
    const MarketExchangeApi& mea,
    int64_t timestamp,
    const std::string& norm_symbol,
    ProductEncyclopedia& product_cache,
    const nlohmann::json* elem = nullptr) {
  nlohmann::json input;
  nlohmann::json output;
  nlohmann::json json;

  std::string unique_name = coin::proto::MarketType_Name(mea.market) + ":" +
                            coin::proto::ExchangeType_Name(mea.exchange) + ":" + norm_symbol;

  auto product_from_uniq_name = CreateProductFromUniqueString(unique_name, timestamp);
  CHECK(product_from_uniq_name);

  auto product = CreateProductFromNormString(mea, norm_symbol, timestamp, false);
  if (product) {
    if (elem) {
      CheckProductParser(*product.get(), *elem);
    }

    CHECK_EQ(product->resolve_timestamp(), timestamp);
    CHECK(product->absolute_norm() == norm_symbol || product->relative_norm() == norm_symbol)
        << "norm_symbol: " << norm_symbol << ", "
        << "absolute_norm: " << product->absolute_norm() << ", "
        << "relative_norm: " << product->relative_norm();

    if (FLAGS_check_holder) {
      auto& holder = product_cache.holder(*product);
      // CHECK_LE(std::abs(holder.product().resolve_timestamp() - timestamp), 30'000'000'000LL)
      //   << holder.product().resolve_timestamp() << ", " << timestamp;
      CHECK_EQ(TimestampToUtc00(holder.product().resolve_timestamp()), TimestampToUtc00(timestamp))
          << holder.product().resolve_timestamp() << ", " << timestamp;

      CHECK_EQ(holder.product().absolute_norm(), product->absolute_norm())
          << holder.product().absolute_norm() << ", " << product->absolute_norm();

      if (mea.exchange == coin::proto::Deribit && mea.market == coin::proto::Options) {
        // pass
      } else {
        CHECK_EQ(holder.product().relative_norm(), product->relative_norm())
            << holder.product().relative_norm() << ", " << product->relative_norm();
      }
    }

    input["timestamp"] = timestamp;
    input["mea"] = mea.String();
    input["norm_symbol"] = norm_symbol;

    output["native_symbol"] = product->native_symbol();
    output["absolute_norm"] = product->absolute_norm();
    output["relative_norm"] = product->relative_norm();
    output["base"] = product->base().currency();
    output["quote"] = product->quote().currency();
    output["native_base"] = product->base().native_currency();
    output["native_quote"] = product->quote().native_currency();
    output["resolve_timestamp"] = product->resolve_timestamp();
  } else {
    CHECK(false) << coin::proto::MarketType_Name(mea.market) + ":" + coin::proto::ExchangeType_Name(mea.exchange) + ":" + norm_symbol;
  }

  json["input"] = input;
  json["output"] = output;
  std::cout << json.dump() << std::endl;
}

void CheckExpiryCalc(
    const MarketExchangeApi& mea,
    int64_t timestamp,
    const std::string& contract_type_str) {
  using coin2::exchange::base::symbology::ContractType;
  using coin2::exchange::base::symbology::FindExpiryFriday;
  using coin2::exchange::base::symbology::ToDateStr;
  ContractType contract_type;
  CHECK(ContractType_Parse(contract_type_str, &contract_type));
  int64_t ts = FindExpiryFriday(mea.exchange, contract_type, timestamp, 8 * 3600 * 1e9);
  std::string date_str = ToDateStr(ts);
  LOG(INFO) << date_str;
}

bool is_digits(const std::string& str) {
  return std::all_of(str.begin(), str.end(), ::isdigit);  // C++11
}

int64_t parse_timestamp(const std::string& ts) {
  using namespace coin2::exchange::base::symbology;
  if (ts.empty()) {
    return GetCurrentTimestamp();
  }

  if (IsDateStr(ts)) {
    std::string temp_ts = ts + "T000000.0000Z";
    return TimestampFromString(temp_ts);
  }

  if (is_digits(ts)) {
    return std::stoll(ts);
  }

  throw std::invalid_argument(ts);
}

std::string GetProductFormatQty(
    const MarketExchangeApi& mea,
    int64_t timestamp,
    const std::string& native_symbol,
    const std::string& norm_symbol,
    ProductEncyclopedia& product_cache,
    double qty) {
  std::unique_ptr<IProduct> ptr_native, ptr_norm;
  std::string native_qty, norm_qty;

  if (!native_symbol.empty()) {
    ptr_native = CreateProductFromNativeString(mea, native_symbol, timestamp, false);
    auto& holder = product_cache.holder(*ptr_native);
    native_qty = holder.product_info().order_formatter().FormatQty(qty);
  }

  if (!norm_symbol.empty()) {
    ptr_norm = CreateProductFromNormString(mea, norm_symbol, timestamp, false);
    auto& holder = product_cache.holder(*ptr_norm);
    norm_qty = holder.product_info().order_formatter().FormatQty(qty);
  }

  if (!native_symbol.empty() && !norm_symbol.empty()) {
    CHECK_EQ(native_qty, norm_qty);
  }

  return native_qty.empty() ? norm_qty : native_qty;
}

std::string GetProductFormatPrice(
    const MarketExchangeApi& mea,
    int64_t timestamp,
    const std::string& native_symbol,
    const std::string& norm_symbol,
    ProductEncyclopedia& product_cache,
    double price) {
  std::unique_ptr<IProduct> ptr_native, ptr_norm;
  std::string native_price, norm_price;

  if (!native_symbol.empty()) {
    ptr_native = CreateProductFromNativeString(mea, native_symbol, timestamp, false);
    auto& holder = product_cache.holder(*ptr_native);
    native_price = holder.product_info().order_formatter().FormatPrice(price);
  }

  if (!norm_symbol.empty()) {
    ptr_norm = CreateProductFromNormString(mea, norm_symbol, timestamp, false);
    auto& holder = product_cache.holder(*ptr_norm);
    norm_price = holder.product_info().order_formatter().FormatPrice(price);
  }

  if (!native_symbol.empty() && !norm_symbol.empty()) {
    CHECK_EQ(native_price, norm_price);
  }

  return native_price.empty() ? norm_price : native_price;
}

int main(int argc, char* argv[]) {
  // TODO(daniel): move google::InitGoogleLogging to coin2::app::InitApplication
  coin2::app::InitApplication(&argc, &argv);

  if (FLAGS_show_version) {
    auto build_info = GetBuildInfo();
    std::cout << "version/revision: " << build_info.revision() << std::endl;
    return 0;
  }

  ProductEncyclopedia product_cache;
  if (FLAGS_json_file.empty()) {
    auto mea = MarketExchangeApi::FromString(FLAGS_mea);
    int64_t timestamp = parse_timestamp(FLAGS_timestamp);
    std::string native_symbol = FLAGS_native_symbol;
    std::string norm_symbol = FLAGS_norm_symbol;

    if (FLAGS_qty || FLAGS_price) {
      if (FLAGS_qty) {
        std::string qty = GetProductFormatQty(
            mea,
            timestamp,
            native_symbol,
            norm_symbol,
            product_cache,
            FLAGS_qty);
        std::cout << "FormatQty " << qty << std::endl;
      }

      if (FLAGS_price) {
        std::string FormatPrice = GetProductFormatPrice(
            mea,
            timestamp,
            native_symbol,
            norm_symbol,
            product_cache,
            FLAGS_price);
        std::cout << "FormatPrice " << FormatPrice << std::endl;
      }

      return 0;
    }

    if (!native_symbol.empty()) {
      TestProductFromNativeString(mea, timestamp, native_symbol, product_cache);
      TestProductFromNativeString(mea, timestamp, native_symbol, product_cache);
    }

    if (!norm_symbol.empty()) {
      TestProductFromNormString(mea, timestamp, norm_symbol, product_cache);
      TestProductFromNormString(mea, timestamp, norm_symbol, product_cache);
    }

    if (!FLAGS_contract_type.empty()) {
      CheckExpiryCalc(mea, timestamp, FLAGS_contract_type);
    }
  } else {
    std::string json_file_name = FLAGS_json_file;
    std::ifstream json_file(json_file_name);
    nlohmann::json json_obj;
    json_file >> json_obj;
    if (json_obj.find("mea") == json_obj.end() ||
        json_obj.find("product_infos") == json_obj.end()) {
      LOG(ERROR) << "Invalid json file";
      return 0;
    }

    auto mea = MarketExchangeApi::FromString(json_obj["mea"].get<std::string>());
    nlohmann::json product_infos = json_obj["product_infos"];
    for (auto elem : product_infos) {
      if (elem.find("fetched_time") == elem.end() || elem.find("symbol") == elem.end() ||
          elem.find("native_symbol") == elem.end()) {
        LOG(ERROR) << "Invalid product info";
        continue;
      }
      int64_t timestamp = elem["fetched_time"];
      std::string native_symbol = elem["native_symbol"].get<std::string>();
      std::string norm_symbol = elem["symbol"].get<std::string>();

      if (!native_symbol.empty()) {
        TestProductFromNativeString(mea, timestamp, native_symbol, product_cache, &elem);
      }

      if (!norm_symbol.empty()) {
        TestProductFromNormString(mea, timestamp, norm_symbol, product_cache, &elem);
      }
    }
  }

  return 0;
}
